/* calligraphica.c */

#include <jtk.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <errno.h>
#include <sys/stat.h>
#include <cairo-pdf.h>

void draw_line(cairo_t *cr, double x1, double y1, double d1,
	           double x2, double y2, double d2);

#define WIDTH   1024
#define HEIGHT  768

const double C = 0.005;
const double K = 4000;
const double RADIUS_DEFAULT = 20.0;
const double RADIUS_MAX     = 20.0;

static int png_file_no = 1;
static int pdf_file_no = 1;

typedef struct point {
    jtk_point     point;
    double        timestamp;
    double        radius;
    struct point *next;
} point;

typedef struct trace {
    point        *point_list;
    struct trace *next;
} trace;

point           *point_list;
trace           *trace_list;
cairo_surface_t *back_surface;
cairo_t         *back_context;
int              is_writing = 0;

void draw_on_paper(cairo_t *cr, point *list);

void point_list_append(point *list, jtk_point p, double timestamp, double radius)
{
    point *node = malloc(sizeof(point));

    node->point = p;
    node->timestamp = timestamp;
    node->radius = radius;
    node->next = list;
    point_list = node;
}

void point_list_clear(point *list)
{
    point *node, *temp;

    node = list;
    while (node != NULL)
    {
        temp = node->next;
        free(node);
        node = temp;
    }
}

void trace_list_append(point *point_list)
{
    trace *node = malloc(sizeof(trace));

    node->point_list = point_list;
    node->next = trace_list;
    trace_list = node;
}

void trace_list_clear()
{
    trace *node, *temp;

    node = trace_list;
    while (node != NULL)
    {
        temp = node->next;
        point_list_clear(node->point_list);
        free(node);
        node = temp;
    }

    trace_list = NULL;
}

void init(int width, int height)
{
    back_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 
                                              width, height);
    back_context = cairo_create(back_surface);
}

double point_distance(jtk_point p1, jtk_point p2)
{
    double a = fabs(p1.x - p2.x);
    double b = fabs(p1.y - p2.y);

    return sqrt(a * 2 + b * 2);
}

double radius_from_event(jtk_event *evt)
{
    double radius = 0, displacement, time;

    displacement = point_list ? point_distance(point_list->point, evt->u.point) : 0;
    time = point_list ? (evt->timestamp - point_list->timestamp) : 1.0;
    radius = (time + C) / displacement * K;

    if (radius >= RADIUS_MAX)
        radius = RADIUS_MAX;

    return radius;
}

void pen_down(void *user)
{
    jtk_event *evt = (jtk_event *) user;
    is_writing = 1;

    fprintf(stderr, "mouse down: (%g, %g, 0.0)\n", 
            evt->u.point.x, evt->u.point.y);

    point_list_append(point_list, evt->u.point, evt->timestamp, RADIUS_DEFAULT);
}

void pen_move(void *user)
{
    jtk_event *evt = (jtk_event *) user;
    double radius = radius_from_event(evt);

    fprintf(stderr, "mouse move: (%g, %g, %g)\n", 
            evt->u.point.x, evt->u.point.y, radius);

    point_list_append(point_list, evt->u.point, evt->timestamp, radius);
    jtk_redraw();
}

void pen_up(void *user)
{
    draw_on_paper(back_context, point_list);

    trace_list_append(point_list);
    point_list = NULL;
    is_writing = 0;
    jtk_redraw();
}

void draw_on_paper(cairo_t *cr, point *list)
{
    point *node, *prev;

    if (list == NULL)
        return;

    prev = list;
    node = prev->next;

    cairo_set_source_rgba(cr, 0.1, 0.1, 0.1, 0.9);
    while (node != NULL)
    {
        draw_line(cr, prev->point.x, prev->point.y, prev->radius,
                  node->point.x, node->point.y, node->radius);

        prev = node;
        node = node->next;
    }
}

void draw_on_screen(void *user)
{
    cairo_t *cr = (cairo_t *) user;

    cairo_save(cr);
    cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
    cairo_set_source_surface(cr, back_surface, 0, 0);
    cairo_paint(cr);
    cairo_restore(cr);

    if (is_writing)
        draw_on_paper(cr, point_list);
}

void clear_paper()
{
    is_writing = 0;
    trace_list_clear();
    point_list = NULL;

    cairo_destroy(back_context);
    cairo_surface_destroy(back_surface);
    init(WIDTH, HEIGHT);

    jtk_redraw();
}

void clean_up()
{
    trace_list_clear();
    point_list = NULL;

    if (back_context)
        cairo_destroy(back_context);

    if (back_surface)
        cairo_surface_destroy(back_surface);

    back_context = NULL;
    back_surface = NULL;
}

#define FILE_NAME_MAX   256

void save_png_to_disk()
{
    char file[FILE_NAME_MAX];
    struct stat st;

try:
    sprintf(file, "cg-%d.png", png_file_no);
    if (stat(file, &st) == 0)
    {
        png_file_no++;
        goto try;
    }

    fprintf(stderr, "save to '%s'\n", file);
    cairo_surface_write_to_png(back_surface, file);
    png_file_no++;
}

void save_pdf_to_disk()
{
    char file[FILE_NAME_MAX];
    struct stat st;

try:
    sprintf(file, "cg-%d.pdf", pdf_file_no);
    if (stat(file, &st) == 0)
    {
        pdf_file_no++;
        goto try;
    }

    fprintf(stderr, "save to '%s'\n", file);
    cairo_surface_t *surface = cairo_pdf_surface_create(file, WIDTH, HEIGHT);
	
    cairo_t *cr = cairo_create(surface);
    trace *list = trace_list;

    while (list != NULL)
    {
        draw_on_paper(cr, list->point_list);
        list = list->next;
    }

    cairo_destroy(cr);
    cairo_surface_destroy(surface);

    pdf_file_no++;
}

void key_down(void *user)
{
    jtk_event *evt = (jtk_event *) user;

    switch (evt->u.key)
    {
    case 'c':
    case 'C':
        clear_paper();
        break;

    case 'q':
    case 'Q':
        clean_up();
        jtk_quit();
        break;

    case 's':
        save_png_to_disk();
        break;

    case 'S':
        save_pdf_to_disk();
        break;

    default:
        break;
    }
}

int main(int argc, char *argv[])
{
    point_list = NULL;
    trace_list = NULL;

    init(WIDTH, HEIGHT);

    jtk_init("Calligraphica", WIDTH, HEIGHT);

    jtk_set_callback(JTK_DRAW, draw_on_screen);
    jtk_set_callback(JTK_MOUSE_DOWN, pen_down);
    jtk_set_callback(JTK_MOUSE_DRAG, pen_move);
    jtk_set_callback(JTK_MOUSE_UP, pen_up);
    jtk_set_callback(JTK_KEY_DOWN, key_down);

    jtk_main_loop();

    return 0;
}

